/**
 * 
 */
package net.fdukedom.chart;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;

import javax.swing.JPanel;

import net.fdukedom.chart.constraints.Constraint;
import net.fdukedom.chart.constraints.EqualsZeroConstraint;
import net.fdukedom.chart.helper.FunctionLib;

/**
 * @author Dmitry Y. Kochelaev
 * @author Fedor Tsarev
 */
public class Chart2D extends JPanel {

	private static final double ARROW_LENGTH = 15;

	private static final double ARROW_WIDTH = 8;

	/**
	 * Background color.
	 */
	private Color background = Color.WHITE;

	/**
	 * Color used to display 2D constraints.
	 */
	private Color constraint2DColor = Color.LIGHT_GRAY;

	/**
	 * Color used to display 1D constraints.
	 */
	private Color constraint1DColor = Color.GRAY;

	/**
	 * Foreground color.
	 */
	private Color foreground = Color.BLACK;

	/**
	 * Maximum of horizontal coordinate.
	 */
	private int maxX = 25;

	/**
	 * Minimum of horizontal coordinate.
	 */
	private int minX = 0;

	/**
	 * Maximum of vertical coordinate.
	 */
	private int maxY = 20;

	/**
	 * Minimum of vertical coordinate.
	 */
	private int minY = -20;

	/**
	 * Horizontal padding.
	 */
	private double paddingX = 10;

	/**
	 * Vertical padding.
	 */
	private double paddingY = 10;

    /**
     * 2D constraints.
     */
    private List<Constraint> constraints2D;

    /**
     * 1D constraint. More than one 1D-constraints
     * do not have sense on 2D plane.
     */
    private Constraint constraint1D;

    /**
     * Function which isolines are to be drawn.
     */
    private Function2D function;
        
    /**
     * Lines.
     */
    private Map<String, List<Dot2D>> lines = new HashMap<String, List<Dot2D>>();

	/**
	 * Creates new chart.
	 */
	public Chart2D() {
		super();
        constraints2D = new ArrayList<Constraint>();
    }

    /**
     * Creates new chart with constraints (difficuilt functions)
     * on background.
     *
     * @param constraints constraints
     */
    public Chart2D(List<Constraint> constraints2D, Constraint constraint1D) {
        super();
        this.constraints2D = constraints2D;
        this.constraint1D = constraint1D;
    }

    public void paint(Graphics g) {
		Graphics2D g2 = (Graphics2D) g;

        g2.setPaint(background);
        g2.fill(new Rectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1));

        drawBackground(g2);

        g2.setPaint(foreground);
        for (String key : lines.keySet()) {
        	drawLine(g2, (List<Dot2D>) lines.get(key));
        }

        drawIsoLines(g2);
        
        drawGrid(g2);
	}

	/**
	 * Adds new line to show.
	 *
	 * @param name name of line. It must be unique.
	 * @param line values.
	 */
	public void addLine(String name, List<Dot2D> line) {
		lines.put(name, Collections.unmodifiableList(line));
		repaint();
	}

	/**
	 * Removes all lines.
	 */
	public void removeAllLines() {
		lines.clear();
	}

	/**
	 * Converts horizontal coordinate to screen coordinate.
	 *
	 * @param value horizontal coordinate.
	 *
	 * @return screen coordinate
	 */
	private double getX(double value) {
		value = (value >= 0) ? Math.min(value, maxX) : Math.max(value, minX);
		return ((getWidth() - 2 * paddingX) / (maxX - minX)) * (value - minX)
				+ paddingX;
	}

    /**
	 * Converts screen coordinate to horizontal coordinate.
	 *
	 * @param value screen coordinate.
	 *
	 * @return horizontal coordinate
	 */
    private double getRealX(int value) {
        double newValue = value - paddingX;
        return newValue / ((getWidth() - 2 * paddingX) / (maxX - minX)) + minX;
    }

    /**
	 * Converts vertical coordinate to screen coordinate.
	 *
	 * @param value vertical coordinate.
	 *
	 * @return screen coordinate
	 */
	private double getY(double value) {
		value = (value >= 0) ? Math.min(value, maxY) : Math.max(value, minY);
		return getHeight() - (((getHeight() - 2 * paddingY) / (maxY - minY))
				* (value - minY) + paddingY);
	}

    /**
	 * Converts screen coordinate to vertical coordinate.
	 *
	 * @param value screen coordinate.
	 *
	 * @return vertical coordinate
	 */
    private double getRealY(int value) {
        return (-value + getHeight() - paddingY) / ((getHeight() - 2 * paddingY) / (maxY - minY)) + minY;
   }

    /**
	 * Draws line.
	 *
	 * @param g graphics to draw on.
	 * @param line line to draw.
	 */
	private void drawLine(Graphics2D g, List<Dot2D> line) {
		// Here line.size() - 1 is used correctly, because
		// there is no line starting in last dot.
		for (int i = 0; i < line.size() - 1; ++i) {
			Dot2D startDot = line.get(i);
			Dot2D endDot = line.get(i + 1);
			drawLine(g, startDot.getX(), startDot.getY(), endDot.getX(), endDot.getY());
		}
	}

	/**
	 * Draws line with default colour. If you want to draw line with your colour
	 * just use <tt>setPaint()</tt> method of <tt>g</tt>
	 *
	 * @param g graphics to draw on.
	 * @param x1 horizontal coordinate of the beginning
	 * @param y1 vertical coordinate of the beginning
	 * @param x2 horizontal coordinate of the end
	 * @param y2 vertical coordinate of the end
	 */
	private void drawLine(Graphics2D g, double x1, double y1, double x2, double y2) {
		g.draw(new Line2D.Double(getX(x1), getY(y1), getX(x2), getY(y2)));
	}

	/**
	 * Draws grid.
	 *
	 * @param g graphics to draw on.
	 */
	private void drawGrid(Graphics2D g) {
		g.setPaint(foreground);
		drawLine(g, minX, 0, maxX, 0);
		drawLine(g, 0, minY, 0, maxY);

		// Draw arrows
		drawEastArrow(g, maxX, 0);
		drawNorthArrow(g, 0, maxY);

	}

	/**
	 * Draws north arrow pointing to the point <code>(x, y)</code>.
	 *
	 * @param g graphics to draw on.
	 * @param x X-coordinate of the end of the arrow.
	 * @param y Y-coordinate of the end of the arrow.
	 */
	private void drawNorthArrow(Graphics2D g, int x, int y) {
		g.draw(new Line2D.Double(getX(x) - ARROW_WIDTH / 2, getY(y) + ARROW_LENGTH, getX(x), getY(y)));
		g.draw(new Line2D.Double(getX(x) + ARROW_WIDTH / 2, getY(y) + ARROW_LENGTH, getX(x), getY(y)));
	}

	/**
	 * Draws east arrow pointing to the point <code>(x, y)</code>.
	 *
	 * @param g graphics to draw on.
	 * @param x X-coordinate of the end of the arrow.
	 * @param y Y-coordinate of the end of the arrow.
	 */
    private void drawEastArrow(Graphics2D g, int x, int y) {
		g.draw(new Line2D.Double(getX(x) - ARROW_LENGTH, getY(y) - ARROW_WIDTH / 2, getX(x), getY(y)));
		g.draw(new Line2D.Double(getX(x) - ARROW_LENGTH, getY(y) + ARROW_WIDTH / 2, getX(x), getY(y)));
	}

	/**
     * Draws background functions.
     *
     * @param g graphics to draw on.
     */
    private void drawBackground(Graphics2D g) {
        Image image = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);

        Graphics2D imageG = (Graphics2D) image.getGraphics();
        imageG.setPaint(background);
        imageG.fill(new Rectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1));

        imageG.setPaint(foreground);

        int width = (int) (this.getWidth() - 2 * paddingX);
        int height = (int) (this.getHeight() - 2 * paddingY);

        boolean[][] toFill2D = new boolean[width][height];
        for (boolean[] t : toFill2D) {
        	Arrays.fill(t, true);
        }

        for (Constraint c : constraints2D) {
        	boolean[][] cur = c.getContainsMap(width, height, minX, maxY, maxX, minY);
        	for (int i = 0; i < width; i++) {
        		for (int j = 0; j < height; j++) {
        			toFill2D[i][j] &= cur[i][j];
        		}
        	}
        }

    	for (int i = 0; i < width; i++) {
    		for (int j = 0; j < height; j++) {
    			int x = (int) paddingX + i;
    			int y = (int) paddingY + j;
    			if (toFill2D[i][j]) {
    				imageG.setColor(constraint2DColor);
                    imageG.drawRect(x, y, 1, 1);
    			}
    		}
    	}

    	boolean[][] toFill1D = constraint1D.getContainsMap(width, height, minX, maxY, maxX, minY);
    	for (int i = 0; i < width; i++) {
    		for (int j = 0; j < height; j++) {
    			int x = (int) paddingX + i;
    			int y = (int) paddingY + j;
    			if (toFill1D[i][j]) {
    				imageG.setColor(constraint1DColor);
                    imageG.drawRect(x, y, 0, 0);
    			}
    		}
    	}

        g.drawImage(image, 0, 0, this.getWidth(), this.getHeight(), new ImageObserver() {
            public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
                return false;
            }
        });
    }
    
    public void drawIsoLines(Graphics2D g) {
    	double min = -10;
    	double max = 10;
    	double step = 1;
        
    	int width = (int) (this.getWidth() - 2 * paddingX);
        int height = (int) (this.getHeight() - 2 * paddingY);

        int rr = 0;
        int gg = 255;
        int bb = 0;
        
        for (double cur = min; cur <= max; cur += step) {
			g.setColor(new Color(rr, gg, bb));
			rr += 11;
			gg -= 11;

			Constraint c = new EqualsZeroConstraint(FunctionLib.subtract(function, FunctionLib.constant(cur)));
        	boolean[][] toFill1D = c.getContainsMap(width, height, minX, maxY, maxX, minY);
        	for (int i = 0; i < width; i++) {
        		for (int j = 0; j < height; j++) {
        			int x = (int) paddingX + i;
        			int y = (int) paddingY + j;
        			if (toFill1D[i][j]) {
                        g.drawRect(x, y, 0, 0);
        			}
        		}
        	}
    	}
    }

    /**
	 * Returns maxX value.
	 *
	 * @return maxX value.
	 */
	public int getMaxX() {
		return maxX;
	}

	/**
	 * Sets maxX.
	 *
	 * @param maxX value to set.
	 */
	public void setMaxX(int maxX) {
		this.maxX = maxX;
		this.invalidate();
	}

	/**
	 * Returns maxY value.
	 *
	 * @return maxY value.
	 */
	public int getMaxY() {
		return maxY;
	}

	/**
	 * Sets maxY.
	 *
	 * @param maxY value to set.
	 */
	public void setMaxY(int maxY) {
		this.maxY = maxY;
		this.invalidate();
	}

	/**
	 * Returns minX value.
	 *
	 * @return minX value.
	 */
	public int getMinX() {
		return minX;
	}

	/**
	 * Sets minX.
	 *
	 * @param minX value to set.
	 */
	public void setMinX(int minX) {
		this.minX = minX;
		this.invalidate();
	}

	/**
	 * Returns minY value.
	 *
	 * @return minY value.
	 */
	public int getMinY() {
		return minY;
	}

	/**
	 * Sets minY.
	 *
	 * @param minY value to set.
	 */
	public void setMinY(int minY) {
		this.minY = minY;
		this.invalidate();
	}

	/**
	 * @return the function
	 */
	public Function2D getFunction() {
		return function;
	}

	/**
	 * @param function the function to set
	 */
	public void setFunction(Function2D function) {
		this.function = function;
	}

	
	
}
